﻿/* ==============================================================================
*
*   Copyright (c) 2010, Pierre Favier
*   All rights reserved.
*
*   NTasking is free software: you can redistribute it and/or modify it
*   under the terms of the GNU General Public License as published by the
*   Free Software Foundation.
*
*   Redistribution and use in source and binary forms, with or without
*   modification, are permitted provided that the following conditions
*   are met:
*
*   -   Redistributions of source code must retain the above copyright notice,
*       this list of conditions and the following disclaimer.
*
*   -   Redistributions in binary form must reproduce the above
*       copyright notice, this list of conditions and the following
*       disclaimer in the documentation and/or other materials
*       provided with the distribution.
*
*   THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
*   WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
*   OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
*   DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
*   BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
*   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
*   TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
*   DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
*   ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
*   TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
*   THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
*   SUCH DAMAGE.
*
* ============================================================================== */

using System;
using System.Collections.Generic;
using System.Linq;
using NTasking.Internals;

namespace NTasking
{

    /// <summary>
    /// The <see cref="Delay"/> class is used for the implementation of the various Ada delay statements
    /// and timed entry calls
    /// </summary>
    public class Delay : Task
    {

        #region Static Methods

        public static SelectEx.AsynchronousSelect Select(DateTime until, Action delayAlternative = null)
        {
            return Instance.WaitUntil.Select(until, delayAlternative).AsAsynchronousSelect();
        }


        public static SelectEx.AsynchronousSelect Select(int milliseconds, Action delayAlternative = null)
        {
            return Instance.WaitUntil.Select(DateTime.Now.AddMilliseconds(milliseconds), delayAlternative).AsAsynchronousSelect();
        }


        public static SelectEx.AsynchronousSelect Select(double seconds, Action delayAlternative = null)
        {
            return Instance.WaitUntil.Select(DateTime.Now.AddSeconds(seconds), delayAlternative).AsAsynchronousSelect();
        }


        public static SelectEx.AsynchronousSelect Select(TimeSpan timeSpan, Action delayAlternative = null)
        {
            return Instance.WaitUntil.Select(DateTime.Now + timeSpan, delayAlternative).AsAsynchronousSelect();
        }

        #endregion


        #region Internal Classes

        /// <summary>
        /// A data structure to store data related to each call to delay.until or delay.for
        /// </summary>
        private struct DelayData
        {

            /// <summary>
            /// The entry call which needs to be accepted
            /// </summary>
            public readonly MethodCall Call;
            /// <summary>
            /// The time at which the entry call can be accepted
            /// </summary>
            public readonly DateTime Schedule;

            /// <summary>
            /// Initializes a new instance of the <see cref="DelayData"/> struct.
            /// </summary>
            /// <param name="call">The entry call which needs to be accepted.</param>
            public DelayData(MethodCall call)
            {
                Call = call;
                Schedule = DateTime.MinValue;
            }

            /// <summary>
            /// Initializes a new instance of the <see cref="DelayData"/> struct.
            /// </summary>
            /// <param name="call">The entry call which needs to be accepted.</param>
            /// <param name="schedule">The time at which the entry call can be accepted.</param>
            public DelayData(MethodCall call, DateTime schedule)
            {
                Call = call;
                Schedule = schedule;
            }

        }


        /// <summary>
        /// A class used to compare two <see cref="DelayData"/> values.
        /// </summary>
        private class DelayDataComparer : IComparer<DelayData>
        {

            /// <summary>
            /// Compares the specified <see cref="DelayData"/> values.
            /// </summary>
            /// <param name="left">The left value.</param>
            /// <param name="right">The right value.</param>
            /// <returns>-1 if left is smaller than right, 0 if left and right are equal and 1 if left is greater than right</returns>
            public int Compare(DelayData left, DelayData right)
            {
                return
                    ReferenceEquals(left.Call, right.Call) ? 0 :
                    left.Schedule <= right.Schedule ? -1 :
                    1;
            }

        }

        #endregion


        #region Fields

        /// <summary>
        /// The singleton <see cref="Delay"/> instance
        /// </summary>
        private static Delay _instance;


        /// <summary>
        /// Ued to compare the various entry calls which are queued
        /// </summary>
        private readonly DelayDataComparer _comparer = new DelayDataComparer();
        /// <summary>
        /// The number of entry calls waiting to be accepted
        /// </summary>
        private int _pendingWaitSize;
        /// <summary>
        /// The maximum size of the array used to store entry calls waiting to be accepted
        /// </summary>
        private int _pendingWaitCapacity = 10;
        /// <summary>
        /// The storage area for entry calls waiting to be accepted
        /// </summary>
        private DelayData[] _pendingWait = new DelayData[10];


        private readonly MethodCall _requeuedCall;

        #endregion


        #region Properties

        /// <summary>
        /// Gets the unique instance of the <see cref="Delay"/> class (singleton).
        /// </summary>
        /// <value>The unique instance of the <see cref="Delay"/> class.</value>
        internal static Delay Instance
        {
            get
            {
                if (Block.Environment == null)
                    throw Errors.NoMasterBlock();

                return _instance;
            }
        }


        internal static void CreateInstance()
        {
            _instance = new Delay();
            _instance.Activate();
        }


        internal static void ClearInstance()
        {
            _instance = null;
        }


        /// <summary>
        /// This entry is used to wait until a given deadline has elapsed.
        /// If the specified time has
        /// already elapsed when the entry is serviced, the caller will be freed immediately.
        /// Otherwise the entry call is requeued to the Wait entry.
        /// </summary>
        [Entry]
        internal Action<DateTime> WaitUntil { get; private set; }


        /// <summary>
        /// This entry is used to simulate waitwaiting for requeued calls which deadline has not elapsed
        /// </summary>
        [Entry]
        private Action Wait { get; set; }

        #endregion


        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="Delay"/> class.
        /// </summary>
        private Delay()
            : base(true)
        {
            Name = "Delay";
            TaskTerminated += DelayTaskTerminated;
            _requeuedCall = ((SynchronizedMethod)Wait.Target).CreateRequeueCall();
        }

        #endregion


        #region Methods

        /// <summary>
        /// Reset the unique instance od the <see cref="Delay"/> class once it has terminated.
        /// </summary>
        /// <param name="reason">The reason.</param>
        /// <param name="exception">The exception.</param>
        private static void DelayTaskTerminated(TaskTerminationReason reason, Exception exception)
        {
            _instance = null;
            if (exception != null)
                Console.WriteLine("Delay threw exception " + exception);
        }


        /// <summary>
        /// Suspends the caller until the specified date has passed.
        /// </summary>
        /// <param name="until">The date to wait for.</param>
        internal static void Until(DateTime until)
        {
            if (until > DateTime.Now)
                Instance.WaitUntil(until);
        }


        /// <summary>
        /// Finishes a synchronized method call.
        /// </summary>
        /// <param name="call">The call to finish.</param>
        /// <param name="signalCaller">When <c>true</c> wake up the caller thread</param>
        internal override void DoFinish(MethodCall call, bool signalCaller)
        {
            int index = Remove(call);

            base.DoFinish(call, signalCaller);

            if (index == 0 && CurrentTask != this)
                // Since the first suspended call has been cancelled we must force the delay
                // task to run again in order to re-evaluate the time at which to wake-up
                // the next suspended task
                ExecutionLock.Pulse();
        }


        /// <summary>
        /// Adds the specified call for suspension until the specified deadline.
        /// </summary>
        /// <param name="call">The call.</param>
        /// <param name="schedule">The deadline.</param>
        private void Add(MethodCall call, DateTime schedule)
        {
            DelayData value = new DelayData(call, schedule);

            // check where the element should be placed
            int index = Array.BinarySearch(_pendingWait, 0, _pendingWaitSize, value, _comparer);
            if (index < 0)
            {
                // xor
                index = ~index;
            }

            if (_pendingWaitSize == _pendingWaitCapacity)
            {
                // Grow the list
                int newCapacity = 2 * _pendingWaitCapacity;
                DelayData[] tempValues = new DelayData[newCapacity];
                if (_pendingWaitSize > 0)
                {
                    // copy only when size is greater than zero
                    Array.Copy(_pendingWait, 0, tempValues, 0, _pendingWaitSize);
                }
                _pendingWait = tempValues;
                _pendingWaitCapacity = newCapacity;
            }

            if (index < _pendingWaitSize)
            {
                Array.Copy(_pendingWait, index, _pendingWait, index + 1, _pendingWaitSize - index);
            }
            _pendingWait[index] = value;
            _pendingWaitSize++;
        }


        /// <summary>
        /// Removes the specified call form the list of suspended calls.
        /// </summary>
        /// <param name="call">The call.</param>
        /// <returns>
        /// Returns the index of the call in the list of suspended calls
        /// or a negative numer if the call was not suspended
        /// </returns>
        private int Remove(MethodCall call)
        {
            DelayData value = new DelayData(call);

            int index = Array.BinarySearch(_pendingWait, 0, _pendingWaitSize, value, _comparer);

            if (index >= 0)
            {
                _pendingWaitSize--;
                Array.Copy(_pendingWait, index + 1, _pendingWait, index, _pendingWaitSize - index);
                _pendingWait[_pendingWaitSize] = default(DelayData);
            }

            return index;
        }


        private void HandleNonRequeuedCall(MethodCall call, bool immediate)
        {
            try
            {
                DateTime delay = ((IParameters<DateTime>)call.Context).P1;

                if (delay < DateTime.Now)
                {
                    // Delay is already passed, No requeue is needed

                    // Simulate Finishing the call normally
                    call.Context.SetResult();
                    Finish(call, true);

                    // Cancel the abortable part or timeout if any
                    call.CancelSelectiveCallAlternative();

                    return;
                }

                using (ExecutionLock.Locked())
                {
                    // Simulate requeueing the call to entry Wait

                    // Mark the call as requeued
                    call.Requeued = _requeuedCall;

                    // Add the call to the list of pending calls
                    PendingCalls.Add(call);

                    // Add the caller to the list of delayed tasks
                    Add(call, delay);
                }
            }
            finally
            {
                if (immediate)
                    call.CallLock.Pulse();

                call.CallLock.ReleaseLock();
            }
        }


        private void HandleNonRequeuedCalls()
        {
            MethodCall[] pendingCalls;

            using (ExecutionLock.Locked())
            {
                pendingCalls = PendingCalls.Where(call => !call.IsRequeued).ToArray();

                foreach (MethodCall call in pendingCalls)
                    PendingCalls.Remove(call);
            }

            foreach (MethodCall call in pendingCalls)
            {
                call.CallLock.GetLock();
                HandleNonRequeuedCall(call, false);
            }
        }


        private DateTime HandleRequeuedCalls()
        {
            DateTime nextDelay = DateTime.MinValue;

            while (_pendingWaitSize > 0)
            {
                DelayData delayData = _pendingWait[0];

                using (delayData.Call.CallLock.Locked())
                {
                    if (delayData.Schedule <= DateTime.Now)
                    {
                        // Simulate an accept(Wait)

                        //Console.WriteLine("One requeud call accepted");

                        delayData.Call.Context.SetResult();
                        Finish(delayData.Call, true);
                    }
                    else
                    {
                        //Console.WriteLine("Will have to wait");

                        nextDelay = delayData.Schedule;
                        break;
                    }
                }

                // Cancel the abortable part or timeout if any
                delayData.Call.CancelSelectiveCallAlternative();
            }

            return nextDelay;
        }


        /// <summary>
        /// Implements the task body.
        /// </summary>
        protected override void Body()
        {
            DateTime nextDelay = DateTime.MinValue;
            RendezVous acceptedRendezVous;

            // Simulate a permanent Accept(WaitUntil)
            ((SynchronizedMethod)WaitUntil.Target).AcceptableCalls.Add(((IExecutable)WaitUntil.Target).CreateContext());

            while (true)
            {

                using (ExecutionLock.Locked())
                {
                    acceptedRendezVous = GetNextRendezVous();

                    if (acceptedRendezVous == null)
                    {
                        if (nextDelay != DateTime.MinValue)
                            acceptedRendezVous = Suspend(nextDelay);
                        else
                            acceptedRendezVous = SuspendOrTerminate();
                    }
                }

                if (acceptedRendezVous != null)
                {
                    HandleNonRequeuedCall(acceptedRendezVous.Call, acceptedRendezVous.Immediate);

                    HandleNonRequeuedCalls();
                }

                nextDelay = HandleRequeuedCalls();

            }
            // ReSharper disable FunctionNeverReturns
        }
        // ReSharper restore FunctionNeverReturns

        #endregion

    }

}
